package org.babyfish.jimmer.apt.client;

import org.babyfish.jimmer.apt.Context;
import org.babyfish.jimmer.apt.GeneratorException;
import org.babyfish.jimmer.client.ExportDoc;
import org.babyfish.jimmer.impl.util.StringUtil;

import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;

public class ExportDocProcessor {

    private static final String JIMMER_DOC = "META-INF/jimmer/doc.properties";

    private final Context context;

    public ExportDocProcessor(Context context) {
        this.context = context;
    }

    public void process(RoundEnvironment roundEnv) {
        Pkg pkg = pkg(roundEnv);
        List<TypeElement> typeElements = new ArrayList<>();
        for (Element element : roundEnv.getRootElements()) {
            if (element instanceof TypeElement) {
                PackageElement packageElement = (PackageElement) element.getEnclosingElement();
                collectTypeElements(
                        pkg.isExported(packageElement.getQualifiedName().toString()),
                        element,
                        typeElements
                );
            }
        }
        if (typeElements.isEmpty()) {
            return;
        }
        writeDoc(typeElements);
    }

    private void collectTypeElements(
            boolean parentExport,
            Element element,
            List<TypeElement> typeElements
    ) {
        if (element instanceof TypeElement) {
            TypeElement typeElement = (TypeElement) element;
            if (typeElement.getKind() == ElementKind.CLASS ||
                    typeElement.getKind() == ElementKind.INTERFACE ||
                    typeElement.getKind() == ElementKind.ENUM) {
                ExportDoc exportDoc = typeElement.getAnnotation(ExportDoc.class);
                boolean export;
                if (exportDoc != null) {
                    export = !exportDoc.excluded();
                } else {
                    export = parentExport;
                }
                if (export) {
                    typeElements.add(typeElement);
                }
                for (Element subElement : typeElement.getEnclosedElements()) {
                    collectTypeElements(
                            export,
                            subElement,
                            typeElements
                    );
                }
            }
        }
    }

    private Pkg pkg(RoundEnvironment roundEnv) {
        Pkg pkg = new Pkg("", null);
        for (Element element : roundEnv.getRootElements()) {
            if (element instanceof TypeElement) {
                pkg.set((PackageElement) element.getEnclosingElement());
            }
        }
        return pkg;
    }

    private void writeDoc(List<TypeElement> typeElements) {
        FileObject fileObject;
        try {
            fileObject = context.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", JIMMER_DOC);
        } catch (IOException ex) {
            throw new GeneratorException(
                    "Cannot read file \"" + JIMMER_DOC + "\" generated in previous compilation",
                    ex
            );
        }
        File docFile = new File(fileObject.getName());
        Properties properties = new Properties();
        if (docFile.exists()) {
            try (Reader reader = Files.newBufferedReader(docFile.toPath(), StandardCharsets.UTF_8)) {
                properties.load(reader);
            } catch (IOException ex) {
                throw new GeneratorException("Cannot read exists file \"" + JIMMER_DOC + "\"", ex);
            }
        }
        for (TypeElement typeElement : typeElements) {
            addProperties(properties, typeElement);
        }
        try (Writer writer = Files.newBufferedWriter(docFile.toPath(), StandardCharsets.UTF_8)) {
            properties.store(writer, "Generated by @" + ExportDoc.class.getName());
        } catch (IOException ex) {
            throw new GeneratorException("Cannot write generated file \"" + JIMMER_DOC + "\"", ex);
        }
    }

    private void addProperties(Properties properties, TypeElement typeElement) {
        String typeName = typeElement.getQualifiedName().toString();
        String doc = standardComment(context.getElements().getDocComment(typeElement));
        if (doc != null) {
            properties.put(typeName, doc);
        }
        for (Element element : typeElement.getEnclosedElements()) {
            if (element.getKind() == ElementKind.FIELD &&
                    !element.getModifiers().contains(Modifier.STATIC)) {
                doc = standardComment(context.getElements().getDocComment(element));
                if (doc != null) {
                    properties.put(typeName + '.' + element.getSimpleName().toString(), doc);
                }
            }
        }
        for (Element element : typeElement.getEnclosedElements()) {
            if (element.getKind() != ElementKind.METHOD || element.getModifiers().contains(Modifier.STATIC)) {
                continue;
            }
            ExecutableElement methodElement = (ExecutableElement) element;
            if (methodElement.getReturnType().getKind() == TypeKind.VOID) {
                continue;
            }
            if (!methodElement.getParameters().isEmpty()) {
                continue;
            }
            doc = standardComment(context.getElements().getDocComment(element));
            if (doc == null) {
                continue;
            }
            String propName = StringUtil.propName(
                    methodElement.getSimpleName().toString(),
                    methodElement.getReturnType().getKind() == TypeKind.BOOLEAN
            );
            properties.put(typeName + '.' + propName, doc);
        }
    }

    private static String standardComment(String comment) {
        if (comment == null) {
            return null;
        }
        comment = comment.trim();
        if (comment.isEmpty()) {
            return null;
        }
        return comment;
    }

    private static class Pkg {
        private static final Pattern DOT_PATTERN = Pattern.compile("\\.");

        private final String name;
        private final Pkg parent;
        private Map<String, Pkg> childMap;

        private Boolean export;

        public Pkg(String name, Pkg parent) {
            this.name = name;
            this.parent = parent;
            this.childMap = null;
        }

        public String getName() {
            return name;
        }

        public Boolean getExport() {
            return export;
        }

        public void setExport(Boolean export) {
            this.export = export;
        }

        public boolean isExported(String packageName) {
            Pkg pkg = sub(packageName, false);
            while (true) {
                if (pkg.export != null) {
                    return pkg.export;
                }
                pkg = pkg.parent;
                if (pkg == null) {
                    break;
                }
            }
            return false;
        }

        public void set(PackageElement packageElement) {
            ExportDoc exportDoc = packageElement.getAnnotation(ExportDoc.class);
            if (exportDoc == null) {
                return;
            }
            Pkg pkg = sub(packageElement.getQualifiedName().toString(), true);
            pkg.export = !exportDoc.excluded();
        }

        public Pkg sub(String packageName, boolean autoCreate) {
            Pkg pkg = this;
            for (String name : DOT_PATTERN.split(packageName)) {
                Pkg childPkg = pkg.child(name, autoCreate);
                if (childPkg == null) {
                    return pkg;
                }
                pkg = childPkg;
            }
            return pkg;
        }

        private Pkg child(String name, boolean autoCreate) {
            if (!autoCreate && childMap == null) {
                return null;
            }
            if (childMap == null) {
                childMap = new LinkedHashMap<>();
            }
            if (!autoCreate) {
                return childMap.get(name);
            }
            return childMap.computeIfAbsent(name, k -> new Pkg(k, this));
        }
    }
}
